Skip to content

Conversation

@DeveloperViraj
Copy link

@DeveloperViraj DeveloperViraj commented Nov 5, 2025

Fixes: #60509

AbortSignal.timeout() previously used internal/timers.setTimeout, which was not intercepted by mock.timers.enable().

This change updates it to use globalThis.setTimeout and globalThis.clearTimeout, so mock.timers.tick() correctly triggers aborts.


/cc @nodejs/testing

Notes for reviewers

This change replaces require('internal/timers') usage with global timers,
so mock.timers in the Node test runner can intercept AbortSignal.timeout.

Verified locally that the fix aligns with issue reproduction steps.

@nodejs-github-bot nodejs-github-bot added the needs-ci PRs that need a full CI run. label Nov 5, 2025
Copy link
Member

@Renegade334 Renegade334 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DOM standard does not use setTimeout for this method, and changes to the public-facing timers API should not be observable here.

The correct route would probably be to add this as a discrete API target for the mocker (ie. enabled via mocks.timers.enable({ apis: ['AbortSignal.timeout'] })) with its own implementation.

@ErickWendel
Copy link
Member

would you also add tests for this feat?

@DeveloperViraj
Copy link
Author

Thanks a lot for the guidance!
I updated the mock implementation to use the separate API target approach (AbortSignal.timeout) as recommended, and added a new test for it.
Please let me know if I should improve or extend anything.

Copy link
Member

@Renegade334 Renegade334 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this! There are a few changes to make, but this is definitely heading in the right direction.

Comment on lines 307 to 313
#storeOriginalAbortSignalTimeout() {
try {
this.#realAbortSignalTimeout = ObjectGetOwnPropertyDescriptor(AbortSignal, 'timeout');
} catch {
this.#realAbortSignalTimeout = undefined;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The store/restore methods should not be allowed to fail silently.

Suggested change
#storeOriginalAbortSignalTimeout() {
try {
this.#realAbortSignalTimeout = ObjectGetOwnPropertyDescriptor(AbortSignal, 'timeout');
} catch {
this.#realAbortSignalTimeout = undefined;
}
}
#storeOriginalAbortSignalTimeout() {
this.#realAbortSignalTimeout = ObjectGetOwnPropertyDescriptor(AbortSignal, 'timeout');
}

Comment on lines 315 to 325
#restoreOriginalAbortSignalTimeout() {
try {
if (this.#realAbortSignalTimeout) {
ObjectDefineProperty(AbortSignal, 'timeout', this.#realAbortSignalTimeout);
} else {
try {
delete AbortSignal.timeout;
} catch {}
}
} catch {}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#restoreOriginalAbortSignalTimeout() {
try {
if (this.#realAbortSignalTimeout) {
ObjectDefineProperty(AbortSignal, 'timeout', this.#realAbortSignalTimeout);
} else {
try {
delete AbortSignal.timeout;
} catch {}
}
} catch {}
}
#restoreOriginalAbortSignalTimeout() {
ObjectDefineProperty(AbortSignal, 'timeout', this.#realAbortSignalTimeout);
}

 true Outdated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please delete this artifact!

Comment on lines 23 to 32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how 0694adb made it into your branch. You will need to drop this commit, using interactive rebase for example.

Comment on lines 633 to 639
/**
* Advances the virtual time of MockTimers by the specified duration (in milliseconds).
* This method simulates the passage of time and triggers any scheduled timers that are due.
* @param {number} [time] - The amount of time (in milliseconds) to advance the virtual time.
* @throws {ERR_INVALID_STATE} If MockTimers are not enabled.
* @throws {ERR_INVALID_ARG_VALUE} If a negative time value is provided.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why these are being removed – unintentional AI action perhaps? Please restore these.

Comment on lines 666 to 674
/**
* @typedef {{apis: SUPPORTED_APIS;now: number | Date;}} EnableOptions Options to enable the timers
* @property {SUPPORTED_APIS} apis List of timers to enable, defaults to all
* @property {number | Date} now The epoch to which the timers should be set to, defaults to 0
*/
/**
* Enables the MockTimers replacing the native timers with the fake ones.
* @param {EnableOptions} [options]
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

internalOptions.apis ||= SUPPORTED_APIS;

validateStringArray(internalOptions.apis, 'options.apis');
// Check that the timers passed are supported
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

Comment on lines 62 to 68
clearTimeout,
setTimeout,
} = require('timers');
clearTimeout: internalClearTimeout,
setTimeout: internalSetTimeout,
} = require('internal/timers');

const clearTimeout = globalThis.clearTimeout;
const setTimeout = globalThis.setTimeout;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert the original changes to lib/internal/abort_controller.

Comment on lines 634 to 671
'AbortSignal.timeout': () => {
this.#storeOriginalAbortSignalTimeout();

const mock = this;

ObjectDefineProperty(AbortSignal, 'timeout', {
__proto__: null,
configurable: true,
writable: true,
value: function mockableAbortSignalTimeout(delay) {
if (NumberIsNaN(delay)) {
throw new ERR_INVALID_ARG_VALUE('delay', delay, 'delay must be a number');
}

const controller = new AbortController();

const timer = mock.#setTimeout(
() => {
try {
controller.abort();
} catch {}
},
delay
);

try {
ObjectDefineProperty(controller.signal, '__mockTimer', {
__proto__: null,
configurable: true,
writable: true,
value: timer,
});
} catch {}

return controller.signal;
},
});
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, nothing here should be able to fail silently; please remove the try/catch blocks.

Comment on lines 659 to 666
try {
ObjectDefineProperty(controller.signal, '__mockTimer', {
__proto__: null,
configurable: true,
writable: true,
value: timer,
});
} catch {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's any need to define this; no underlying timer is exposed on true AbortSignals, and the timeout is protected from GC while it remains active in the execution queue.

@Renegade334 Renegade334 added semver-minor PRs that contain new features and should be released in the next minor version. test_runner Issues and PRs related to the test runner subsystem. labels Nov 15, 2025
@DeveloperViraj DeveloperViraj force-pushed the fix/mock-timers-abortsignal-timeout branch from c30dce3 to 735ee27 Compare November 15, 2025 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-ci PRs that need a full CI run. semver-minor PRs that contain new features and should be released in the next minor version. test_runner Issues and PRs related to the test runner subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mock.timers ignores AbortSignal.timeout

4 participants